Skip to content

fix(client): reject a mismatched sampleRate or format instead of ignoring it - #953

Open
chinmayv095 wants to merge 2 commits into
elevenlabs:mainfrom
chinmayv095:fix/webrtc-device-option-parity
Open

fix(client): reject a mismatched sampleRate or format instead of ignoring it#953
chinmayv095 wants to merge 2 commits into
elevenlabs:mainfrom
chinmayv095:fix/webrtc-device-option-parity

Conversation

@chinmayv095

@chinmayv095 chinmayv095 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Fixes #549.

The premise still holds, under a different name

The issue names setInputDevice, which no longer exists — the public entry point is now VoiceConversation.changeInputDevice() (and useConversation().changeInputDevice() in @elevenlabs/react), delegating to InputController.setDevice. Verified against main: the inconsistency it describes is unchanged.

changeInputDevice accepts Partial<FormatConfig> & InputDeviceConfig and forwards sampleRate, format and preferHeadphonesForIosDevices straight through:

  • WebSocket (MediaDeviceInput.setDevice) ignored sampleRate/format unconditionally — they cannot be applied to an already-running AudioContext — and switched the device.
  • WebRTC (WebRTCConnection.input.setDevice) threw on sampleRate/format/preferHeadphonesForIosDevices unconditionally, and did not change the device at all.

The output controllers had the same asymmetry for sampleRate/format.

What this does

Both connection types now compare the requested sampleRate/format against the one the connection actually negotiated (the WebSocket path's own configured values; WebRTC's fixed pcm_48000), input and output:

  • A value that differs from what's negotiated throws, on both connection types. sampleRate/format are things the caller explicitly controls; silently ignoring a value we cannot honor was the actual bug in the original WebSocket behavior, not just the WebRTC/WebSocket inconsistency by itself. See this thread for the full reasoning after @kraenhansen's review.
  • Re-passing the same value the connection already has, alongside a device id or alone, stays a no-op on both connection types. Callers do this routinely — index.test.ts already had a test asserting changeInputDevice({ sampleRate: 16000, format: "pcm" }) and the output equivalent succeed with no deviceId, passing the session's own format back rather than asking to change it. That existing pattern still works.

preferHeadphonesForIosDevices stays a silent best-effort hint on WebRTC (unchanged from before): it's a device-selection preference, not a format guarantee the way sampleRate/format are.

Tests

WebRTCConnection.test.ts: 4 tests asserting throw on a real mismatch (input sampleRate, input format, output sampleRate+format, each with a device id present), 2 tests asserting the re-passed-same-value case still switches the device.

input.test.ts/output.test.ts (WebSocket path): throw-on-mismatch and no-op-on-match, exercised directly against MediaDeviceInput/MediaDeviceOutput. output.test.ts is new — MediaDeviceOutput had no test coverage before, and its create() needs a live AudioContext/AudioWorklet that isn't available outside this repo's browser test project (only index.test.ts and input.test.ts are configured to run there). The guard runs before setDevice touches any instance state, so it's tested against the class's real setDevice without going through create(), avoiding adding output creation to the browser suite for this fix alone.

Fail-first with only the three source files stashed: the 3 throw-assertion tests fail (rest are unaffected, since they test the no-op path). packages/client 227/227 with the fix restored. packages/react 140/140, unaffected. turbo lint/check-types/build green across all 9 packages (29/29). Changeset included, patch on @elevenlabs/client.

No test pinned the old throwing/ignoring behaviour as-is, and nothing in packages/react or packages/react-native needed changing — both call straight through.

Heads-up on overlap: #642, #813 and #814 also touch WebRTCConnection.ts, but none of them touches either setDevice, and the hunks here are small and adjacent.

@kraenhansen

Copy link
Copy Markdown
Member

I see that I wrote the original issue a bit vague - sorry for that 🤔 Not a huge fan of our SDK swallowing this. If a user pass in a specific sampleRate or format and this is not supported, it should clearly throw - not just ignore as this is something the developer is in control of and they've provided a clear intent that we cannot match.

@cursor

cursor Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

PR Summary

Medium Risk
Changes runtime behavior for callers who relied on WebSocket silently ignoring format fields during device switches; WebRTC callers who re-pass session format gain compatibility. Core audio/device paths are affected but guarded by tests.

Overview
changeInputDevice / changeOutputDevice now treat sampleRate and format the same on WebSocket and WebRTC: if the caller passes values that differ from what the connection was created with, both paths throw instead of WebSocket silently ignoring them or WebRTC rejecting even a re-pass of the active format.

WebSocket MediaDeviceInput / MediaDeviceOutput remember the configured sampleRate / format at creation and enforce that in setDevice. WebRTC WebRTCConnection compares against negotiated inputFormat / outputFormat (e.g. pcm_48000) with the same rules. Re-sending the current sampleRate/format (with or without a device id) remains a no-op on both paths. preferHeadphonesForIosDevices on WebRTC is no longer rejected and stays a silent hint when only that flag is passed.

Tests cover throw-on-mismatch, successful device switch when format matches, and preferHeadphonesForIosDevices behavior; @elevenlabs/client gets a patch changeset.

Reviewed by Cursor Bugbot for commit 10733c9. Bugbot is set up for automated code reviews on this repo. Configure here.

@chinmayv095

Copy link
Copy Markdown
Contributor Author

Correct, and fixed in 147f731.

Ignoring was the wrong direction. sampleRate and format are things the developer is explicitly in control of, and if the value they asked for cannot be applied, swallowing that silently is the actual bug, not the inconsistency by itself. Both connection types now throw on a value they cannot honor instead.

The throw is on the value, not the field name. changeInputDevice/changeOutputDevice re-forward whatever the caller passes, and index.test.ts already had a test exercising changeInputDevice({ sampleRate: 16000, format: "pcm" }) and changeOutputDevice({ sampleRate: 16000, format: "pcm" }) with no deviceId, asserting success. That call is a real, existing pattern of a caller passing the session's own format back rather than asking to change it, and I did not want to break it to satisfy this. So the guard compares against what the connection actually negotiated (the WebSocket path's own sampleRate/format, and WebRTC's fixed pcm_48000): re-passing that same pair, alongside a device id or alone, stays a no-op, exactly as it did before. Asking for a different value throws on both paths, on both input and output.

preferHeadphonesForIosDevices is left as a silent best-effort hint, unchanged from the earlier ignore behavior. It's a device-selection preference rather than a format guarantee the way sampleRate/format are, and it wasn't part of what you flagged, so I didn't fold a second policy change into this one.

4 tests updated in WebRTCConnection.test.ts to assert throw instead of ignore on a real mismatch, plus 2 new tests confirming the re-passed-same-value case still switches the device. Added input.test.ts/output.test.ts coverage for the WebSocket-path guard directly (output.test.ts is new; MediaDeviceOutput had no test file before, and its create() needs a live AudioContext that isn't available outside the browser test project, so the guard is exercised against the class's own setDevice without going through create()).

Fail-first with only the three source files stashed: the 3 tests asserting the new throw fail (2 pass regardless, since they test the unchanged no-op case). packages/client 227/227 with the fix restored (packages/react 140/140, unaffected). turbo lint/check-types/build green across all 9 packages. Changeset updated to describe the corrected behavior.

@chinmayv095 chinmayv095 changed the title fix(client): ignore unsupported device options on the WebRTC path fix(client): reject a mismatched sampleRate or format instead of ignoring it Aug 19, 2026
changeInputDevice forwards sampleRate, format and
preferHeadphonesForIosDevices to the input controller. The WebSocket
path ignores all three, because they cannot be applied to an
already-running AudioContext, and switches the device. The WebRTC path
threw instead, so the same call switched the microphone on one
connection type and failed outright on the other, and the device was
never changed. The output controllers differed the same way for
sampleRate and format.

Both WebRTC controllers now ignore the options they cannot apply, which
is what makes the two connection types substitutable behind
InputController and OutputController.
…ring it

@kraenhansen pointed out that ignoring an explicit sampleRate/format on
setDevice hides a request the SDK cannot honor, when the caller has
given a clear signal we cannot match. Both connection types now throw
when the requested value differs from the one the connection already
negotiated, on WebRTC as well as WebSocket, input and output.

Re-passing the connection's own current sampleRate/format alongside a
device id, which callers do routinely and which an existing test in
index.test.ts already exercised, stays a no-op: the value is not
actually being changed, so there is no unmatched intent to reject.
preferHeadphonesForIosDevices stays a silent best-effort hint on
WebRTC, unchanged, since it was not part of this complaint and is not
a format guarantee the way sampleRate/format are.
@chinmayv095
chinmayv095 force-pushed the fix/webrtc-device-option-parity branch from 147f731 to 10733c9 Compare August 19, 2026 23:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Align sampleRate handling across connection types in setInputDevice

2 participants